Alteryx の Python ツールで PDFファイルから表を抽出してみる
Alteryx の Pythonツールを使って PDFファイル内の表を読み込んでみたので、その手順を記事にしました。
前提条件
以下の環境を使用しています。
- Windows 10 Pro
- Alteryx Designer 2023.1.1.247
Python を使用して PDFファイルから表を抽出するにあたり、ここでは Python のライブラリである「tabula-py」を使用します。
tabula-py は、Java のライブラリとして提供されている tabula の wrapper になります。そのため Python の他に端末への Java のインストールが必要です。
Python、Java のバージョンに関する条件として以下があります。
- Java
- Java 8+
- Python
- 3.8+
参考:tabula-py
ここでは Java の環境用意から進めます。
Java のインストール
インストーラーの実行
こちら(Java Downloads for All Operating Systems)からインストールを行います。Windows 環境ですので「Windows Offline(64-bit)」をインストールします。
インストーラーを起動し、表示される画面に沿って進めます。ここでは、インストール先のディレクトリは変更しませんでした。
インストールが完了すると以下の表示となります。
バージョンも以下の通り確認できます。
JAVA_HOME 環境変数の設定
Alteryx から tabula-py を使用するために JAVA_HOME 環境変数の設定を行います。
bin フォルダがあるパスを環境変数として設定します。ここではシステム環境変数として、以下の通り設定しました。
- 環境変数名
- JAVA_HOME
- 変数値
- C:\Program Files\Java\jre-1.8
環境変数は Alteryx からも確認できます。
環境変数の設定後、Alteryx を再起動し、フォーミュラツール等から以下の関数を実行することで変数値を取得できます。
GetEnvironmentVariable("JAVA_HOME")
出力例
C:\Program Files\Java\jre-1.8
ライブラリのインストール
次に Alteryx の Python ツールからライブラリを追加します。
Python ツールを配置し、以下のコマンドを実行します。
from ayx import Package Package.installPackages("tabula-py")
問題なく実行できると「Successfully installed distro-1.8.0 jpype1-1.4.1 tabula-py-2.8.2」などと表示されます。 エラーが発生した場合は Alteryx Designer を管理者権限で実行した上で、再度インストールをお試しください。
参考:
AlteryxのPythonツールでパッケージを入れるときに必要な権限について | DevelopersIO
How To: Use Alteryx.installPackages() in Python tool | Alteryx Knowledge Base
PDF の読み取り
罫線のある表の読み取り
検証用に以下のようなPDFを用意し tabula で表部分を読み取ってみます。
ワークフローは下図の通りです。
ディレクトリツールで読み取りたい PDF ファイルパスを取得後、ファイルパスが「\」で区切られているのを「/」に置換しています。
こちらの手順については、以下に詳しく記載があるので、こちらもご参照ください。
Alteryx の Pythonツールを使って PDFファイルからテキストを抽出してみた ~超シンプルファイル編~ | DevelopersIO
Python ツールでは以下のコードを実行しています。
from ayx import Alteryx import tabula ## PATHを取得 filepath = Alteryx.read("#1") ## PDF を読み取り dfs = tabula.read_pdf(input_path = filepath.iat[0,0], lattice = True, pages = '1') ## 「#1」アンカーに出力 Alteryx.write(dfs[0],1)
read_pdf
関数で表を読み取ります。ここでのオプションは以下の通りです。
- input_path:読み取る対象のファイルを指定します。(例:
"/path/to/sample.pdf"
) - lattice:各セルを区切る罫線がある場合、
True
を指定します。 - pages:抽出するページを指定します。(例:
'1-2,3'
、'all'
)
引数の詳細は以下に記載があります。 High level interfaces | tabula
関数の出力は Pandas DataFrame のリストとして返されます。(なので dfs[0] として出力しています。)
複数の表を抽出する際や、表の一部しか出力されない場合などは、このリストの長さを確認しながら進めるとよいかと思います。
ここでは、Python ツールの出力として以下のように表形式でデータを取得できていました。
なお、JAVA_HOME パスを正しく設定できていない場合、以下のようなエラーメッセージが表示されます。この場合、パスが正しく設定されているかご確認ください。
JVMNotFoundException: No JVM shared library file (jvm.dll) found. Try setting up the JAVA_HOME environment variable properly.
罫線のない表の読み取り
表形式ではあるものの、下図のように罫線がない場合もあるかと思います。このような表を読み取ってみます。
ワークフローとしては読み込み対象のファイルと、Python ツールでのPDF読み込み部分を以下に変更しています。
## PDF を読み取り dfs = tabula.read_pdf(input_path = filepath.iat[0,0], lattice = False, pages = '1')
変更点としてはlattice = False
とし、罫線のない表を読み取るオプションとしています。
Python ツールの出力として以下のようになり、もとのPDFでヘッダとデータ部分に距離があるように見えるせいか、ヘッダ部分が正しく取得できていませんでした。
しかし、この程度であれば、Alteryx で以降の処理を追加すれば容易に修正・加工できます。
ページ内に複数の表がある場合(罫線あり)
次は以下のようにページ内に複数の表があるPDFを読み込んでみます。
この場合も、ワークフローの構成は変更しておらず、読み込み対象のファイルと、Python ツールでのコード部分を変更しています。
from ayx import Alteryx import tabula ## PATHを取得 filepath = Alteryx.read("#1") ## PDF を読み取り dfs = tabula.read_pdf(input_path = filepath.iat[0,0], lattice = True, pages = '1') Alteryx.write(dfs[0],1) Alteryx.write(dfs[1],2)
ポイントとしては、罫線があるのでlattice = True
とし、各表を出力するアンカーを指定しています。
出力は以下の通りとなり、表の構造が異なっても正しく読み取ってくれました。
- 出力1
- 出力2
ページ内に複数の表がある場合(罫線なし)
次は下図のように罫線がない表でも試してみます。表の形式は先と同様なので、同じ出力が返ってくることを期待します。
ワークフローとしては読み込み対象のファイルと、Python ツールでのPDF読み込み部分を以下に変更しています。
## PDF を読み取り dfs = tabula.read_pdf(input_path = filepath.iat[0,0], lattice = False, pages = '1')
※変更点:lattice = False
とし、罫線のない表を読み取るオプションとしています。
出力は以下の通りとなっていました。表の順番が変わっていたり、列名が正しく取得できていなかったりしています。
- 出力1
- 出力2
この程度であれば、Alteryx で加工・修正は可能ですが、次に後述する方法で意図する出力を得られるか試してみます。
ページ内の特定の範囲から抽出する
tabula.read_pdf
ではarea
というオプションでPDFページの中でも、解析対象の範囲を指定可能です。このオプションで範囲を絞り、指定の領域のみを抽出することで、意図する形で読み取れないか試してみます。
まず対象のページのサイズを取得します。コードとしては以下の内容で、必要に応じて Python ツール内の Jupyter Notebook から実行します。
ポイントとしてtabula.read_pdf
のオプションで guess = False
とすることでページ内のすべての範囲を解析対象としています。(デフォルトは True
)
またoutput_format="json"
とすることで area に関する情報を取得できるようにしています。
from ayx import Alteryx import tabula ## PATHを取得 filepath = Alteryx.read("#1") ## PDFのAreaを取得 dfs = tabula.read_pdf(input_path = filepath.iat[0,0], lattice=False, pages='1',guess = False, output_format="json") top = dfs[0]["top"] left = dfs[0]["left"] bottom = dfs[0]["height"] + top right = dfs[0]["width"] + left print(top, bottom, left, right)
出力は以下のようになります。
0.0 841.7999877929688 0.0 595.4000244140625
ここでは、左から順に top, bottom, left, right なので、ページの左上を起点に範囲を指定すればよいことがわかります。
この部分、厳密に area の情報を取得できればよいのですが、ここではサイズを調整しつつ何度か試した結果として、以下のコードを実行してみます。
from ayx import Alteryx import tabula ## PATHを取得 filepath = Alteryx.read("#1") ## PDFのAreaを取得 dfs = tabula.read_pdf(input_path = filepath.iat[0,0], lattice=False, pages='1',guess = False, output_format="json") top = dfs[0]["top"] left = dfs[0]["left"] bottom = dfs[0]["height"] + top right = dfs[0]["width"] + left ## Area を定義 area = [top + 180, left , bottom - 400, right] ## PDF を読み取り df = tabula.read_pdf(input_path = filepath.iat[0,0], lattice=False, pages='1',guess= False, area = area) Alteryx.write(df[0],1)
引数のarea
で、もとのページ全体から指定の範囲のみを読み取るイメージです。 (ここでは下図の赤枠部分)
この場合、出力は以下のようになりました。
[Age]列がずれてしまっていますが、この程度なら Alteryx 上で容易に修正できます。
同様の手段で、ページ内のもう一つの表を抽出することも可能です。
複数ページの表を取得する
さいごに、複数ページの表を抽出してみます。
PDF ファイルとしては下図のような形式です。(2ページ表示しています。)
実行したコードは以下になります。
from ayx import Alteryx import tabula ## PATHを取得 filepath = Alteryx.read("#1") ## PDF を読み取り dfs = tabula.read_pdf(input_path = filepath.iat[0,0], lattice = False, pages = 'all') Alteryx.write(dfs[0],1) Alteryx.write(dfs[1],2)
罫線がないのでlattice
オプションはFalse
とし、pages
オプションをall
としています。
これにより、複数のページ(ここではすべて)を抽出対象としています。
出力は下図のようになります。
- 出力1
- 出力2
こちらもヘッダ部分の抽出がうまくいきませんでしたが、以降の処理で修正は可能です。
または、上述の手順のように area を指定する手もあるかと思います。
ここでは Python ツールの各出力アンカーに結果を出力しています。
今回の例の場合、ヘッダも異なるのでもう一工夫必要ですが、表構造が同じであれば事前に Python ツール側でユニオンなどの処理を行うことで、出力アンカーの数以上のページや表であっても Alteryx 側に渡すことは可能です。
さいごに
PDF からの表抽出をいくつかのパターンで試してみました。
実際には扱う PDF ファイルの内容により処理や抽出の難易度も異なりますが、他にもオプションがあるので目的の部分のみ抽出できないか試す価値はあると感じました。
こちらの内容がその際の参考になれば幸いです。